feat: replace ref-based messages with component-based message bubbles#555
Conversation
Replace the ref-based message array with modular MessageBubble and MessageList components for better state management and readability. - Add messageBubble.js: memo-wrapped component with imperative update() API via useImperativeHandle for streaming content updates - Add messageList.js: container managing bubble instances, imperative APIs (addMessage, updateMessage, clear, setMessages), scroll throttle, and auto-scroll behavior - Refactor conversationPanel.js: thin wrapper delegating to MessageList - Refactor app.js: remove messages array state, forceRender, and manual scroll logic, delegate entirely to messageListRef imperatives
Implement pub/sub architecture where each MessageBubble subscribes to
its own topic (msg-{id}). MessageList publishes streaming updates that
bubble catches and appends its own chunks array, triggering independent
re-renders without parent re-render. This solves the streaming gap bug
where app.js called updateMessage() but the bubble never updated mid-stream.
- Add createPubSub() factory in messageBubble.js for testable pub/sub
- Add PubSubContext for React context propagation
- Replace forceRenderKeyRef in MessageList with topicsRef + subscribe/publish
- MessageBubble uses useContext + useEffect subscription with dedup
- Add 12 pubsub unit tests and 11 messageListApi simulation tests
|
Pub/sub fix committed (5a27498):
This solves the streaming gap bug where called but the bubble never updated mid-stream because mutating refs doesn't trigger React reconciliation. |
Spread syntax requires children to be an array, but React children can be a single element, null, or other non-array types. Wrap with toArray() to guarantee an iterable array before spreading.
The TUI message bubble refactoring was implemented with a pub/sub topic system instead of the original forwardRef + useImperativeHandle ref callback approach. This simplifies the architecture by eliminating the need for MessageList to track bubble instance refs. - Replaced ref callback pattern with unique pub/sub topics per message - MessageBubble uses chunk accumulation via useState instead of streamingId - Scroll ref accessed via getScrollRef() instead of prop forwarding - Added PubSub API requirements and scenarios to specs - Updated design doc with pub/sub rationale and trade-offs
Remove the inline memo-wrapped MessageBubble component and its associated imports from conversationPanel.js. The module now delegates entirely to the component-based MessageList with pub/sub. Update conversationPanel.test.js and tui.test.js to remove stale renderMessages tests and replace them with MessageList integration tests. - Removed MessageBubble memo wrapper and related imports - Removed renderMessages utility and associated tests - 302 lines deleted, 61 added
Remove the inline memo-wrapped MessageBubble component and its associated imports from conversationPanel.js. The module now delegates entirely to the component-based MessageList with pub/sub. Update conversationPanel.test.js and tui.test.js to remove stale renderMessages tests and replace them with MessageList integration tests. - Removed MessageBubble memo wrapper and related imports - Removed renderMessages utility and associated tests - 302 lines deleted, 61 added
…to conversationPanel test
New fix: preserve conversation history on stream interruptionWhen a user sends a message while the agent is streaming, This removes the |
Post-Audit ResultsGoal Fulfillment
Spec Compliance
Quality Checks
Deviations from Plan
Risks / Known Issues
|
- Create openspec/specs/component-message-bubbles/spec.md from archived delta (all ADDED, no header mismatch issues) - Reconcile openspec/specs/tui-scroll-view/spec.md: replace all ConversationPanel scroll responsibilities with MessageList (ownership moved from data-driven to imperative ref-based) - Update archived delta .openspec.yaml with reconciliation timestamp
Delta Spec Reconciliation NoteThe archived delta specs (openspec archive) were unable to auto-sync because:
Reconciliation done on this branch (commit
The archived delta files are preserved in |
|
Closes #541 |
Summary
Replace the ref-based message array architecture in the TUI with a component-based system where each message is a standalone MessageBubble component managing its own state.
Changes
src/tui/messageBubble.js: MessageBubble component with internal state management (content, streaming, toolCallDisplay, reasoningContent). Exposesupdate()method for streaming events.src/tui/messageList.js: Manages array of MessageBubble instances withaddMessage(),updateMessage(),clear()methods. Handles MAX_RENDER_MESSAGES windowing and scroll-to-bottom.src/tui/conversationPanel.js: Simplified to thin wrapper rendering MessageList.src/tui/app.js: RemovedmessagesRef,forceRender,renderCount. Uses MessageList instance.Motivation
The current architecture uses messagesRef (mutable array) + forceRender (useState counter) + array spreading + useEffect scroll-to-bottom. This fights React's model and causes memory leaks, unnecessary re-renders, and unreliable scroll management.
Fixes #541